iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
Modern Web

Phoenix 1.7 完全教學系列 第 4

4 快速上手夠用的 Elixir (Pattern matching)

  • 分享至 

  • xImage
  •  

在使用 Pattern matching (模式比對) 的時候,都會想起給小朋友玩的形狀比對積木

https://ithelp.ithome.com.tw/upload/images/20230919/20141054kjEuqgT5Cu.png

Match operator (=)

Elixir 在設定變數的時候就是 Pattern matching 了

name = "Jack"

在這邊 "Jack" 這個積木被放到沒有任何限制的 name 變數

這個行為在取出複雜結構的時候特別好用,例如 Map

%{name: "Jack", age: age_of_jack} = %{name: "Jack", age: 34}
age_of_jack
#=> 34

我們可以試著想像左邊這個 Map 挖了一個洞是 age_of_jack
被我們拿來比對右邊,符合的項目就是 34

如果 name 的值不一樣呢?

%{name: "Finn", age: age_of_jack} = %{name: "Jack", age: 34}

** (MatchError) no match of right hand side value: %{name: "Jack", age: 34}
這個時候就會跳出 MatchError 告訴你比對失敗

那這樣呢?

1 = 1

在其他的語言, = 是作為賦予變數右邊的值,所以通常這樣寫都會錯誤,
但在 Elixir 裡是 pattern matching,我們拿右邊的值來跟左邊比較,如果有變數的洞再填符合的項目進去,
1 = 1 這個情況,就是 pattern matching 成功,而左邊沒有任何空位需要補,所以忽略。
但是如果寫 1 = 3 這樣子就會 MatchError 了。

Matching Tuple

在 match Tuple 的時候,我們需要確定兩邊的長度一致
很多函式會回傳 {:ok, 結果} 這種結構的 tuple,
我們就可以用 pattern matching 把結果接起來

{:ok, result} = {:ok, "Jack"}
result
#=> "Jack"

{:ok, users} = {:ok, [%{name: "Jack"}, %{name: "Finn"}]}
users
#=> [%{name: "Jack"}, %{name: "Finn"}]

Matching List

與 tuple 一樣,list 需要一樣的長度才會成功

[1, x] = [1, 2]
x
#=> 2

在 Elixir 的 list , 因為他是 Linked list ,從前面取值比較有效率 (原理之後有篇幅再說明)
所以 Elixir 提供一個給 List 用的特殊匹配法,就算不知道長度也可以使用

[head | tail] = [1, 2, 3, 4, 5]
head
#=> 1
tail
#=> [2, 3, 4, 5]

在左邊使用 [第一項 | 剩下的] 這個結構來取出 list 的第一項,與剩餘的 list
這個結構在寫遞迴的時候特別好用(之後說明)

Matching Map

因為 Map 的 key 不能重複,所以在使用 Map 匹配的時候可以有只拿出特定值的效果
這邊有一種濾網的感覺

very_large_map = %{a: 2343, b: 34234, c: 234234, d: 325, e: 355}
%{a: a} = very_large_map
a
#=> 2343

另外,這樣也是可以的,從右邊的 = 執行,開始中間那段可以想成,有 match 成功,但是只有拿出 a,整個的值還是一樣
所以 very_large_map 一樣包含全部的值

very_large_map = %{a: a} = %{a: 2343, b: 34234, c: 234234, d: 325, e: 355}

very_large_map
#=>  %{a: 2343, b: 34234, c: 234234, d: 325, e: 355}
a
#=> 2343

呼叫函式時 matching

在前一篇我們提到,Elixir 允許定義同名且同 arity 的方法 (arity: 變數數量),如下

defmodule Greet do
  def hi(%{name: name, level: 1}) do
    "你好 #{name}"
  end
  
  def hi(%{name: name, level: 2}) do
    "你好 #{name}, 會員等級 2 的會員享有半價折扣"
  end
  
  def hi(_) do
    "你誰?"
  end
end

Greet.hi(%{name: Jack, level: 2})
#=> "你好 Jack, 會員等級 2 的會員享有半價折扣"

這個模式滿常見的,可以利用 pattern matching 的特性從 Map 快速濾出要執行的是哪一個方法,
當我們呼叫 Greet.hi(%{name: Jack, level: 2}) 的時候,
Elixir 會先找到所有的 hi 方法,並開始從上至下開始一個一個試著 match,
他會先套套看第一個 hi(%{name: name, level: 1}) 但發現 level 不符合
接著套套看第二個 hi(%{name: name, level: 2}) ,這一個就符合了,
所以會執行這一個方法,忽略剩下的。
如果全部的同名方法都不符合,會出現 MatchError 錯誤。

如果需要的話,可以在最後一個方法使用一個絕對可以 match 的單個變數來把它收起來並做出相對應的回應
如果想要 match 不是預期中的變數,也不需要使用該變數的話,可以用 _ 把他接起來,並不再使用。

小測驗

寫一個可以產生一下結果的 module
member 的格式為 %{name: 字串名稱, level: 數字等級},因應會員等級有不同的折扣
如果會員格式錯誤就回傳 "Error"

member_a = %{name: "Jack", level: 3}
Store.discount(member_a)
#=> "30% off!"

member_b = %{name: "Finn", level: 2}
Store.discount(member_b)
#=> "20% off!"

member_c = %{name: "PB", level: 3}
Store.discount(member_c)
#=> "You are PB!, 99% off!"

member_d = {:error, "member not found"}
Store.discount(member_d)
#=> "Error"

參考解答

defmodule Store do
  def discount(%{name: "PB"}) do
    # 小陷阱,這個特殊的 VIP 應該要放在比 level 3 前面,
    # 要是放在後面就會被 level 3 匹配回應就錯了。
    "You are PB!, 99% off!"
  end
  
  def discount(%{level: 3}) do
    "30% off!"
  end
  
  def discount(%{level: 2}) do
    "20% off!"
  end
  
  def discount(_) do
    "Error"
  end
end

上一篇
3 快速上手夠用的 Elixir (模組, 函式)
下一篇
5 快速上手夠用的 Elixir (條件判斷)
系列文
Phoenix 1.7 完全教學30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言